{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Rag Dolls and Tapestries\n", "\n", "In this experiment, we will explore Object-Oriented Programming by examining Rag Dolls and Tapestries.\n", "\n", "In computer simulations, a rag doll is a stick figure. However, the stick figure is constructed in such a way that the joints can move independently, but the body parts remain connected. In fact, the same idea can be used for any item that we wish to simulate that has joints connecting points of mass. We'll see that rag dolls and tapestries can both be constructed from the same elements.\n", "\n", "To allow body parts to both move independently, and also remain connected, and to make curtains, we have to use some mathematics. To make it fast, we use a technique called _verlet integration_. \n", "\n", "## Points and Links" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This code is based on http://gamedevelopment.tutsplus.com/tutorials/simulate-tearable-cloth-and-ragdolls-with-simple-verlet-integration--gamedev-519\n", "\n", "We need to basic classes:\n", "\n", "1. PointMass - a point in 2D space, with some mass\n", "2. Link - a rigid connection between two PointMasses\n", "\n", "Out of these two classes, we can build all kinds of things, including:\n", "\n", "1. Circle\n", "1. Body (uses Circle for head)\n", "2. Tapestry - a curtain-like matrix composed of a mesh of PointMasses and Links\n", "\n", "This uses the same ideas from the bouncing ball examples, with some minor differences:\n", "\n", "* velocity will be computed based on last_position - current_position\n", "* we will take mass into account\n", "* objects can't move freely... they may have constraints due to their links to other PointMasses" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ "\n", "
\n", " Sketch #12:
\n", "
\n", "
\n", "
\n", " \n", " \n", " \n", " \n", "
\n", "Sketch #12 state: Loading...
\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "// PointMass\n", "class PointMass {\n", " float lastX, lastY; // for calculating position change (velocity)\n", " float x,y;\n", " float accX, accY;\n", " \n", " float mass = 1;\n", " float damping = 20;\n", "\n", " // An ArrayList for links, so we can have as many links as we want to this PointMass\n", " ArrayList links = new ArrayList();\n", " \n", " boolean pinned = false;\n", " float pinX, pinY;\n", " \n", " // PointMass constructor\n", " PointMass(float xPos, float yPos) {\n", " x = xPos;\n", " y = yPos;\n", " \n", " lastX = x;\n", " lastY = y;\n", " \n", " accX = 0;\n", " accY = 0;\n", " }\n", " \n", " // The update function is used to update the physics of the PointMass.\n", " // motion is applied, and links are drawn here\n", " void updatePhysics(float timeStep) { // timeStep should be in elapsed seconds (deltaTime)\n", " this.applyForce(0, mass * gravity);\n", " \n", " float velX = x - lastX;\n", " float velY = y - lastY;\n", " \n", " // dampen velocity\n", " velX *= 0.99;\n", " velY *= 0.99;\n", "\n", " float timeStepSq = timeStep * timeStep;\n", "\n", " // calculate the next position using Verlet Integration\n", " float nextX = x + velX + 0.5 * accX * timeStepSq;\n", " float nextY = y + velY + 0.5 * accY * timeStepSq;\n", " \n", " // reset variables\n", " lastX = x;\n", " lastY = y;\n", " \n", " x = nextX;\n", " y = nextY;\n", " \n", " accX = 0;\n", " accY = 0;\n", " }\n", " \n", " void updateInteractions() {\n", " // this is where our interaction comes in.\n", " if (mousePressed) {\n", " float distanceSquared = distPointToSegmentSquared(pmouseX,pmouseY,mouseX,mouseY,x,y);\n", " if (mouseButton == LEFT) {\n", " if (distanceSquared < mouseInfluenceSize) { // remember mouseInfluenceSize was squared in setup()\n", " // To change the velocity of our PointMass, we subtract that change from the lastPosition.\n", " // When the physics gets integrated (see updatePhysics()), the change is calculated\n", " // Here, the velocity is set equal to the cursor's velocity\n", " lastX = x - (mouseX-pmouseX)*mouseInfluenceScalar;\n", " lastY = y - (mouseY-pmouseY)*mouseInfluenceScalar;\n", " }\n", " }\n", " else { // if the right mouse button is clicking, we tear the cloth by removing links\n", " if (distanceSquared < mouseTearSize) \n", " links.clear();\n", " }\n", " }\n", " }\n", "\n", " void draw() {\n", " // draw the links and points\n", " stroke(0);\n", " if (links.size() > 0) {\n", " for (int i = 0; i < links.size(); i++) {\n", " Link currentLink = (Link) links.get(i);\n", " currentLink.draw();\n", " }\n", " }\n", " else\n", " point(x, y);\n", " }\n", " \n", " /* Constraints */\n", " void solveConstraints() {\n", " /* Link Constraints */\n", " // Links make sure PointMasss connected to this one is at a set distance away\n", " for (int i = 0; i < links.size(); i++) {\n", " Link currentLink = (Link) links.get(i);\n", " currentLink.solve();\n", " }\n", " \n", " /* Boundary Constraints */\n", " // These if statements keep the PointMasss within the screen\n", " if (y < 1)\n", " y = 2 * (1) - y;\n", " if (y > height-1)\n", " y = 2 * (height - 1) - y;\n", " \n", " if (x > width-1)\n", " x = 2 * (width - 1) - x;\n", " if (x < 1)\n", " x = 2 * (1) - x;\n", " \n", " /* Other Constraints */\n", " // make sure the PointMass stays in its place if it's pinned\n", " if (pinned) {\n", " x = pinX;\n", " y = pinY; \n", " }\n", " }\n", " \n", " // attachTo can be used to create links between this PointMass and other PointMasses\n", " void attachTo(PointMass P, float restingDist, float stiff) {\n", " attachTo(P, restingDist, stiff, 30, true);\n", " }\n", " void attachTo(PointMass P, float restingDist, float stiff, boolean drawLink) {\n", " attachTo(P, restingDist, stiff, 30, drawLink);\n", " }\n", " void attachTo(PointMass P, float restingDist, float stiff, float tearSensitivity) {\n", " attachTo(P, restingDist, stiff, tearSensitivity, true);\n", " }\n", " void attachTo(PointMass P, float restingDist, float stiff, float tearSensitivity, boolean drawLink) {\n", " Link lnk = new Link(this, P, restingDist, stiff, tearSensitivity, drawLink);\n", " links.add(lnk);\n", " }\n", " void removeLink (Link lnk) {\n", " links.remove(lnk);\n", " } \n", " \n", " void applyForce(float fX, float fY) {\n", " // acceleration = (1/mass) * force\n", " // or\n", " // acceleration = force / mass\n", " accX += fX/mass;\n", " accY += fY/mass;\n", " }\n", " \n", " void pinTo (float pX, float pY) {\n", " pinned = true;\n", " pinX = pX;\n", " pinY = pY;\n", " }\n", "} \n", "\n", "// The Link class is used for handling distance constraints between PointMasses.\n", "class Link {\n", " float restingDistance;\n", " float stiffness;\n", " float tearSensitivity;\n", " \n", " PointMass p1;\n", " PointMass p2;\n", " \n", " // if you want this link to be invisible, set this to false\n", " boolean drawThis = true;\n", " \n", " Link(PointMass which1, PointMass which2, float restingDist, float stiff, float tearSensitivity, boolean drawMe) {\n", " p1 = which1; // when you set one object to another, it's pretty much a reference. \n", " p2 = which2; // Anything that'll happen to p1 or p2 in here will happen to the paticles in our ArrayList\n", " \n", " restingDistance = restingDist;\n", " stiffness = stiff;\n", " drawThis = drawMe;\n", " \n", " this.tearSensitivity = tearSensitivity;\n", " }\n", " \n", " // Solve the link constraint\n", " void solve() {\n", " // calculate the distance between the two PointMasss\n", " float diffX = p1.x - p2.x;\n", " float diffY = p1.y - p2.y;\n", " float d = sqrt(diffX * diffX + diffY * diffY);\n", " \n", " // find the difference, or the ratio of how far along the restingDistance the actual distance is.\n", " float difference = (restingDistance - d) / d;\n", " \n", " // if the distance is more than curtainTearSensitivity, the cloth tears\n", " if (d > tearSensitivity) \n", " p1.removeLink(this);\n", " \n", " // Inverse the mass quantities\n", " float im1 = 1 / p1.mass;\n", " float im2 = 1 / p2.mass;\n", " float scalarP1 = (im1 / (im1 + im2)) * stiffness;\n", " float scalarP2 = stiffness - scalarP1;\n", " \n", " // Push/pull based on mass\n", " // heavier objects will be pushed/pulled less than attached light objects\n", " p1.x += diffX * scalarP1 * difference;\n", " p1.y += diffY * scalarP1 * difference;\n", " \n", " p2.x -= diffX * scalarP2 * difference;\n", " p2.y -= diffY * scalarP2 * difference;\n", " }\n", "\n", " // Draw if it's visible\n", " void draw() {\n", " if (drawThis)\n", " line(p1.x, p1.y, p2.x, p2.y);\n", " }\n", "}\n", "\n", "// Used as a head for ragdolls\n", "class Circle {\n", " float radius;\n", " PointMass attachedPointMass;\n", " \n", " Circle (float r) {\n", " radius = r;\n", " }\n", " \n", " // Constraints\n", " void solveConstraints () {\n", " float x = attachedPointMass.x;\n", " float y = attachedPointMass.y;\n", " \n", " // only do a boundary constraint\n", " if (y < radius)\n", " y = 2*(radius) - y;\n", " if (y > height-radius)\n", " y = 2 * (height - radius) - y;\n", " if (x > width-radius)\n", " x = 2 * (width - radius) - x;\n", " if (x < radius)\n", " x = 2*radius - x;\n", " \n", " attachedPointMass.x = x;\n", " attachedPointMass.y = y;\n", " }\n", " \n", " void draw () {\n", " ellipse(attachedPointMass.x, attachedPointMass.y, radius*2, radius*2);\n", " }\n", " \n", " void attachToPointMass (PointMass p) {\n", " attachedPointMass = p;\n", " }\n", "}\n", "\n", "// Body\n", "// Here we construct and store a ragdoll\n", "class Body {\n", " /*\n", " O\n", " /|\\\n", " / | \\\n", " / \\\n", " | |\n", " */\n", " PointMass head;\n", " PointMass shoulder;\n", " PointMass elbowLeft;\n", " PointMass elbowRight;\n", " PointMass handLeft;\n", " PointMass handRight;\n", " PointMass pelvis;\n", " PointMass kneeLeft;\n", " PointMass kneeRight;\n", " PointMass footLeft;\n", " PointMass footRight;\n", " Circle headCircle;\n", " \n", " float headWidth;\n", " float headLength;\n", " Body (float x, float y, float bodyHeight) {\n", " headLength = bodyHeight / 7.5;\n", " headWidth = headLength * 3/4;\n", "\n", " head = new PointMass(x + random(-5,5),y + random(-5,5));\n", " head.mass = 4;\n", " shoulder = new PointMass(x + random(-5,5),y + random(-5,5));\n", " shoulder.mass = 26; // shoulder to torso\n", " head.attachTo(shoulder, 5/4 * headLength, 1, bodyHeight*2, true);\n", " \n", " elbowLeft = new PointMass(x + random(-5,5),y + random(-5,5));\n", " elbowRight = new PointMass(x + random(-5,5),y + random(-5,5));\n", " elbowLeft.mass = 2; // upper arm mass\n", " elbowRight.mass = 2; \n", " elbowLeft.attachTo(shoulder, headLength*3/2, 1, bodyHeight*2, true);\n", " elbowRight.attachTo(shoulder, headLength*3/2, 1, bodyHeight*2, true);\n", " \n", " handLeft = new PointMass(x + random(-5,5),y + random(-5,5));\n", " handRight = new PointMass(x + random(-5,5),y + random(-5,5));\n", " handLeft.mass = 2;\n", " handRight.mass = 2;\n", " handLeft.attachTo(elbowLeft, headLength*2, 1, bodyHeight*2, true);\n", " handRight.attachTo(elbowRight, headLength*2, 1, bodyHeight*2, true);\n", " \n", " pelvis = new PointMass(x + random(-5,5),y + random(-5,5));\n", " pelvis.mass = 15; // pelvis to lower torso\n", " pelvis.attachTo(shoulder,headLength*3.5,0.8,bodyHeight*2, true);\n", " // this restraint keeps the head from tilting in extremely uncomfortable positions\n", " pelvis.attachTo(head, headLength*4.75, 0.02, bodyHeight*2, false);\n", " \n", " kneeLeft = new PointMass(x + random(-5,5),y + random(-5,5));\n", " kneeRight = new PointMass(x + random(-5,5),y + random(-5,5));\n", " kneeLeft.mass = 10;\n", " kneeRight.mass = 10;\n", " kneeLeft.attachTo(pelvis, headLength*2, 1, bodyHeight*2, true);\n", " kneeRight.attachTo(pelvis, headLength*2, 1, bodyHeight*2, true);\n", " \n", " footLeft = new PointMass(x + random(-5,5),y + random(-5,5));\n", " footRight = new PointMass(x + random(-5,5),y + random(-5,5));\n", " footLeft.mass = 5; // calf + foot\n", " footRight.mass = 5;\n", " footLeft.attachTo(kneeLeft, headLength*2, 1, bodyHeight*2, true);\n", " footRight.attachTo(kneeRight, headLength*2, 1, bodyHeight*2, true);\n", " \n", " // these constraints resist flexing the legs too far up towards the body\n", " footLeft.attachTo(shoulder, headLength*7.5, 0.001, bodyHeight*2, false);\n", " footRight.attachTo(shoulder, headLength*7.5, 0.001, bodyHeight*2, false);\n", " \n", " headCircle = new Circle(headLength*0.75);\n", " headCircle.attachToPointMass(head);\n", " \n", " world.addCircle(headCircle);\n", " addPointMass(head);\n", " addPointMass(shoulder);\n", " addPointMass(pelvis);\n", " addPointMass(elbowLeft);\n", " addPointMass(elbowRight);\n", " addPointMass(handLeft);\n", " addPointMass(handRight);\n", " addPointMass(kneeLeft);\n", " addPointMass(kneeRight);\n", " addPointMass(footLeft);\n", " addPointMass(footRight);\n", " }\n", " \n", " void removeFromWorld () {\n", " world.removeCircle(headCircle);\n", " removePointMass(head);\n", " removePointMass(shoulder);\n", " removePointMass(pelvis);\n", " removePointMass(elbowLeft);\n", " removePointMass(elbowRight);\n", " removePointMass(handLeft);\n", " removePointMass(handRight);\n", " removePointMass(kneeLeft);\n", " removePointMass(kneeRight);\n", " removePointMass(footLeft);\n", " removePointMass(footRight);\n", " }\n", "}\n", "\n", "// Timesteps are managed here\n", "class World {\n", " // list of circle constraints\n", " ArrayList circles = new ArrayList();\n", " \n", " long previousTime;\n", " long currentTime;\n", " \n", " int fixedDeltaTime;\n", " float fixedDeltaTimeSeconds;\n", " \n", " int leftOverDeltaTime;\n", " \n", " int constraintAccuracy;\n", " \n", " World() {\n", " fixedDeltaTime = 16;\n", " fixedDeltaTimeSeconds = (float)fixedDeltaTime / 1000.0;\n", " leftOverDeltaTime = 0;\n", " constraintAccuracy = 3;\n", " }\n", " \n", " // Update physics\n", " void update() {\n", " // calculate elapsed time\n", " currentTime = millis();\n", " long deltaTimeMS = currentTime - previousTime;\n", " \n", " previousTime = currentTime; // reset previous time\n", " \n", " // break up the elapsed time into manageable chunks\n", " int timeStepAmt = (int)((float)(deltaTimeMS + leftOverDeltaTime) / (float)fixedDeltaTime);\n", " \n", " // limit the timeStepAmt to prevent potential freezing\n", " timeStepAmt = min(timeStepAmt, 5);\n", " \n", " // store however much time is leftover for the next frame\n", " leftOverDeltaTime = (int)deltaTimeMS - (timeStepAmt * fixedDeltaTime);\n", " \n", " // How much to push PointMasses when the user is interacting\n", " mouseInfluenceScalar = 1.0 / timeStepAmt;\n", " \n", " // update physics\n", " for (int iteration = 1; iteration <= timeStepAmt; iteration++) {\n", " // solve the constraints multiple times\n", " // the more it's solved, the more accurate.\n", " for (int x = 0; x < constraintAccuracy; x++) {\n", " for (int i = 0; i < pointmasses.size(); i++) {\n", " PointMass pointmass = (PointMass) pointmasses.get(i);\n", " pointmass.solveConstraints();\n", " }\n", " for (int i = 0; i < circles.size(); i++) {\n", " Circle c = (Circle) circles.get(i);\n", " c.solveConstraints(); \n", " }\n", " }\n", " \n", " // update each PointMass's position\n", " for (int i = 0; i < pointmasses.size(); i++) {\n", " PointMass pointmass = (PointMass) pointmasses.get(i);\n", " pointmass.updateInteractions();\n", " pointmass.updatePhysics(fixedDeltaTimeSeconds);\n", " }\n", " }\n", " }\n", " \n", " void addCircle (Circle c) {\n", " circles.add(c); \n", " }\n", " void removeCircle (Circle c) {\n", " circles.remove(c); \n", " }\n", "}\n", "\n", "\n", "// Where we'll store all of the points\n", "ArrayList pointmasses;\n", "\n", "// every PointMass within this many pixels will be influenced by the cursor\n", "float mouseInfluenceSize = 20; \n", "// minimum distance for tearing when user is right clicking\n", "float mouseTearSize = 8;\n", "float mouseInfluenceScalar = 5;\n", "\n", "// amount to accelerate everything downward\n", "float gravity = 980; \n", "\n", "// Dimensions for our curtain. These are number of PointMasss for each direction, not actual widths and heights\n", "// the true width and height can be calculated by multiplying restingDistances by the curtain dimensions\n", "final int curtainHeight = 40;\n", "final int curtainWidth = 60;\n", "final int yStart = 25; // where will the curtain start on the y axis?\n", "final float restingDistances = 6;\n", "final float stiffnesses = 1;\n", "final float curtainTearSensitivity = 50; // distance the PointMasss have to go before ripping\n", "\n", "World world;\n", "\n", "void setup() {\n", " size(500,250);\n", " world = new World();\n", " mouseInfluenceSize = 20; \n", " // minimum distance for tearing when user is right clicking\n", " mouseTearSize = 8;\n", " mouseInfluenceScalar = 5;\n", " // we square the mouseInfluenceSize and mouseTearSize so we don't have to use squareRoot when comparing distances with this.\n", " mouseInfluenceSize *= mouseInfluenceSize; \n", " mouseTearSize *= mouseTearSize;\n", " reset();\n", "}\n", "\n", "void draw() {\n", " background(255); \n", " world.update();\n", " updateGraphics();\n", "}\n", "\n", "void updateGraphics() {\n", " for (PointMass p : pointmasses) {\n", " p.draw();\n", " }\n", " for (Circle c : world.circles) {\n", " c.draw(); \n", " }\n", "}\n", "\n", "void addPointMass(PointMass p) {\n", " pointmasses.add(p); \n", "}\n", "\n", "void removePointMass(PointMass p) {\n", " pointmasses.remove(p); \n", "}\n", "\n", "void createCurtain() {\n", " // midWidth: amount to translate the curtain along x-axis for it to be centered\n", " // (curtainWidth * restingDistances) = curtain's pixel width\n", " int midWidth = (int) (width/2 - (curtainWidth * restingDistances)/2);\n", " // Since this our fabric is basically a grid of points, we have two loops\n", " for (int y = 0; y <= curtainHeight; y++) { // due to the way PointMasss are attached, we need the y loop on the outside\n", " for (int x = 0; x <= curtainWidth; x++) { \n", " PointMass pointmass = new PointMass(midWidth + x * restingDistances, y * restingDistances + yStart);\n", " \n", " // attach to \n", " // x - 1 and\n", " // y - 1 \n", " // *<---*<---*<-..\n", " // ^ ^ ^\n", " // | | |\n", " // *<---*<---*<-..\n", " //\n", " // PointMass attachTo parameters: PointMass PointMass, float restingDistance, float stiffness\n", " // try disabling the next 2 lines (the if statement and attachTo part) to create a hairy effect\n", " if (x != 0) \n", " pointmass.attachTo((PointMass)(pointmasses.get(pointmasses.size()-1)), restingDistances, stiffnesses);\n", " // the index for the PointMasss are one dimensions, \n", " // so we convert x,y coordinates to 1 dimension using the formula y*width+x \n", " if (y != 0)\n", " pointmass.attachTo((PointMass)(pointmasses.get((y - 1) * (curtainWidth+1) + x)), restingDistances, stiffnesses);\n", " // we pin the very top PointMasss to where they are\n", " if (y == 0)\n", " pointmass.pinTo(pointmass.x, pointmass.y);\n", " // add to PointMass array \n", " pointmasses.add(pointmass);\n", " }\n", " }\n", "}\n", "\n", "void createBodies(int count) {\n", " for (int i = 0; i < count; i++) {\n", " new Body(random(width), random(height), 80);\n", " } \n", "}\n", "\n", "// Controls. The r key resets the curtain, g toggles gravity\n", "void keyPressed() {\n", " if ((key == 'r') || (key == 'R')) {\n", " reset();\n", " } \n", " if ((key == 'g') || (key == 'G'))\n", " toggleGravity();\n", "}\n", "\n", "void toggleGravity() {\n", " if (gravity != 0)\n", " gravity = 0;\n", " else\n", " gravity = 980;\n", "}\n", "\n", "// Using http://www.codeguru.com/forum/showpost.php?p=1913101&postcount=16\n", "// We use this to have consistent interaction\n", "// so if the cursor is moving fast, it won't interact only in spots where the applet registers it at\n", "float distPointToSegmentSquared(float lineX1, float lineY1, float lineX2, float lineY2, float pointX, float pointY) {\n", " float vx = lineX1 - pointX;\n", " float vy = lineY1 - pointY;\n", " float ux = lineX2 - lineX1;\n", " float uy = lineY2 - lineY1;\n", " \n", " float len = ux*ux + uy*uy;\n", " float det = (-vx * ux) + (-vy * uy);\n", " if ((det < 0) || (det > len)) {\n", " ux = lineX2 - pointX;\n", " uy = lineY2 - pointY;\n", " return min(vx*vx+vy*vy, ux*ux+uy*uy);\n", " }\n", " \n", " det = ux*vy - uy*vx;\n", " return (det*det) / len;\n", "}\n", "\n", "void reset() {\n", " pointmasses = new ArrayList();\n", " world.circles = new ArrayList();\n", " //createCurtain();\n", " createBodies(20);\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the downloadable version of Processing, you can put each class in its own tab." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ "\n", "
\n", " Sketch #1:
\n", "
\n", "
\n", "
\n", " \n", " \n", " \n", " \n", "
\n", "Sketch #1 state: Loading...
\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "// PointMass\n", "class PointMass {\n", " float lastX, lastY; // for calculating position change (velocity)\n", " float x,y;\n", " float accX, accY;\n", " \n", " float mass = 1;\n", " float damping = 20;\n", "\n", " // An ArrayList for links, so we can have as many links as we want to this PointMass\n", " ArrayList links = new ArrayList();\n", " \n", " boolean pinned = false;\n", " float pinX, pinY;\n", " \n", " // PointMass constructor\n", " PointMass(float xPos, float yPos) {\n", " x = xPos;\n", " y = yPos;\n", " \n", " lastX = x;\n", " lastY = y;\n", " \n", " accX = 0;\n", " accY = 0;\n", " }\n", " \n", " // The update function is used to update the physics of the PointMass.\n", " // motion is applied, and links are drawn here\n", " void updatePhysics(float timeStep) { // timeStep should be in elapsed seconds (deltaTime)\n", " this.applyForce(0, mass * gravity);\n", " \n", " float velX = x - lastX;\n", " float velY = y - lastY;\n", " \n", " // dampen velocity\n", " velX *= 0.99;\n", " velY *= 0.99;\n", "\n", " float timeStepSq = timeStep * timeStep;\n", "\n", " // calculate the next position using Verlet Integration\n", " float nextX = x + velX + 0.5 * accX * timeStepSq;\n", " float nextY = y + velY + 0.5 * accY * timeStepSq;\n", " \n", " // reset variables\n", " lastX = x;\n", " lastY = y;\n", " \n", " x = nextX;\n", " y = nextY;\n", " \n", " accX = 0;\n", " accY = 0;\n", " }\n", " \n", " void updateInteractions() {\n", " // this is where our interaction comes in.\n", " if (mousePressed) {\n", " float distanceSquared = distPointToSegmentSquared(pmouseX,pmouseY,mouseX,mouseY,x,y);\n", " if (mouseButton == LEFT) {\n", " if (distanceSquared < mouseInfluenceSize) { // remember mouseInfluenceSize was squared in setup()\n", " // To change the velocity of our PointMass, we subtract that change from the lastPosition.\n", " // When the physics gets integrated (see updatePhysics()), the change is calculated\n", " // Here, the velocity is set equal to the cursor's velocity\n", " lastX = x - (mouseX-pmouseX)*mouseInfluenceScalar;\n", " lastY = y - (mouseY-pmouseY)*mouseInfluenceScalar;\n", " }\n", " }\n", " else { // if the right mouse button is clicking, we tear the cloth by removing links\n", " if (distanceSquared < mouseTearSize) \n", " links.clear();\n", " }\n", " }\n", " }\n", "\n", " void draw() {\n", " // draw the links and points\n", " stroke(0);\n", " if (links.size() > 0) {\n", " for (int i = 0; i < links.size(); i++) {\n", " Link currentLink = (Link) links.get(i);\n", " currentLink.draw();\n", " }\n", " }\n", " else\n", " point(x, y);\n", " }\n", " \n", " /* Constraints */\n", " void solveConstraints() {\n", " /* Link Constraints */\n", " // Links make sure PointMasss connected to this one is at a set distance away\n", " for (int i = 0; i < links.size(); i++) {\n", " Link currentLink = (Link) links.get(i);\n", " currentLink.solve();\n", " }\n", " \n", " /* Boundary Constraints */\n", " // These if statements keep the PointMasss within the screen\n", " if (y < 1)\n", " y = 2 * (1) - y;\n", " if (y > height-1)\n", " y = 2 * (height - 1) - y;\n", " \n", " if (x > width-1)\n", " x = 2 * (width - 1) - x;\n", " if (x < 1)\n", " x = 2 * (1) - x;\n", " \n", " /* Other Constraints */\n", " // make sure the PointMass stays in its place if it's pinned\n", " if (pinned) {\n", " x = pinX;\n", " y = pinY; \n", " }\n", " }\n", " \n", " // attachTo can be used to create links between this PointMass and other PointMasses\n", " void attachTo(PointMass P, float restingDist, float stiff) {\n", " attachTo(P, restingDist, stiff, 30, true);\n", " }\n", " void attachTo(PointMass P, float restingDist, float stiff, boolean drawLink) {\n", " attachTo(P, restingDist, stiff, 30, drawLink);\n", " }\n", " void attachTo(PointMass P, float restingDist, float stiff, float tearSensitivity) {\n", " attachTo(P, restingDist, stiff, tearSensitivity, true);\n", " }\n", " void attachTo(PointMass P, float restingDist, float stiff, float tearSensitivity, boolean drawLink) {\n", " Link lnk = new Link(this, P, restingDist, stiff, tearSensitivity, drawLink);\n", " links.add(lnk);\n", " }\n", " void removeLink (Link lnk) {\n", " links.remove(lnk);\n", " } \n", " \n", " void applyForce(float fX, float fY) {\n", " // acceleration = (1/mass) * force\n", " // or\n", " // acceleration = force / mass\n", " accX += fX/mass;\n", " accY += fY/mass;\n", " }\n", " \n", " void pinTo (float pX, float pY) {\n", " pinned = true;\n", " pinX = pX;\n", " pinY = pY;\n", " }\n", "} \n", "\n", "// The Link class is used for handling distance constraints between PointMasses.\n", "class Link {\n", " float restingDistance;\n", " float stiffness;\n", " float tearSensitivity;\n", " \n", " PointMass p1;\n", " PointMass p2;\n", " \n", " // if you want this link to be invisible, set this to false\n", " boolean drawThis = true;\n", " \n", " Link(PointMass which1, PointMass which2, float restingDist, float stiff, float tearSensitivity, boolean drawMe) {\n", " p1 = which1; // when you set one object to another, it's pretty much a reference. \n", " p2 = which2; // Anything that'll happen to p1 or p2 in here will happen to the paticles in our ArrayList\n", " \n", " restingDistance = restingDist;\n", " stiffness = stiff;\n", " drawThis = drawMe;\n", " \n", " this.tearSensitivity = tearSensitivity;\n", " }\n", " \n", " // Solve the link constraint\n", " void solve() {\n", " // calculate the distance between the two PointMasss\n", " float diffX = p1.x - p2.x;\n", " float diffY = p1.y - p2.y;\n", " float d = sqrt(diffX * diffX + diffY * diffY);\n", " \n", " // find the difference, or the ratio of how far along the restingDistance the actual distance is.\n", " float difference = (restingDistance - d) / d;\n", " \n", " // if the distance is more than curtainTearSensitivity, the cloth tears\n", " if (d > tearSensitivity) \n", " p1.removeLink(this);\n", " \n", " // Inverse the mass quantities\n", " float im1 = 1 / p1.mass;\n", " float im2 = 1 / p2.mass;\n", " float scalarP1 = (im1 / (im1 + im2)) * stiffness;\n", " float scalarP2 = stiffness - scalarP1;\n", " \n", " // Push/pull based on mass\n", " // heavier objects will be pushed/pulled less than attached light objects\n", " p1.x += diffX * scalarP1 * difference;\n", " p1.y += diffY * scalarP1 * difference;\n", " \n", " p2.x -= diffX * scalarP2 * difference;\n", " p2.y -= diffY * scalarP2 * difference;\n", " }\n", "\n", " // Draw if it's visible\n", " void draw() {\n", " if (drawThis)\n", " line(p1.x, p1.y, p2.x, p2.y);\n", " }\n", "}\n", "\n", "// Used as a head for ragdolls\n", "class Circle {\n", " float radius;\n", " PointMass attachedPointMass;\n", " \n", " Circle (float r) {\n", " radius = r;\n", " }\n", " \n", " // Constraints\n", " void solveConstraints () {\n", " float x = attachedPointMass.x;\n", " float y = attachedPointMass.y;\n", " \n", " // only do a boundary constraint\n", " if (y < radius)\n", " y = 2*(radius) - y;\n", " if (y > height-radius)\n", " y = 2 * (height - radius) - y;\n", " if (x > width-radius)\n", " x = 2 * (width - radius) - x;\n", " if (x < radius)\n", " x = 2*radius - x;\n", " \n", " attachedPointMass.x = x;\n", " attachedPointMass.y = y;\n", " }\n", " \n", " void draw () {\n", " ellipse(attachedPointMass.x, attachedPointMass.y, radius*2, radius*2);\n", " }\n", " \n", " void attachToPointMass (PointMass p) {\n", " attachedPointMass = p;\n", " }\n", "}\n", "\n", "// Body\n", "// Here we construct and store a ragdoll\n", "class Body {\n", " /*\n", " O\n", " /|\\\n", " / | \\\n", " / \\\n", " | |\n", " */\n", " PointMass head;\n", " PointMass shoulder;\n", " PointMass elbowLeft;\n", " PointMass elbowRight;\n", " PointMass handLeft;\n", " PointMass handRight;\n", " PointMass pelvis;\n", " PointMass kneeLeft;\n", " PointMass kneeRight;\n", " PointMass footLeft;\n", " PointMass footRight;\n", " Circle headCircle;\n", " \n", " float headWidth;\n", " float headLength;\n", " Body (float x, float y, float bodyHeight) {\n", " headLength = bodyHeight / 7.5;\n", " headWidth = headLength * 3/4;\n", "\n", " head = new PointMass(x + random(-5,5),y + random(-5,5));\n", " head.mass = 4;\n", " shoulder = new PointMass(x + random(-5,5),y + random(-5,5));\n", " shoulder.mass = 26; // shoulder to torso\n", " head.attachTo(shoulder, 5/4 * headLength, 1, bodyHeight*2, true);\n", " \n", " elbowLeft = new PointMass(x + random(-5,5),y + random(-5,5));\n", " elbowRight = new PointMass(x + random(-5,5),y + random(-5,5));\n", " elbowLeft.mass = 2; // upper arm mass\n", " elbowRight.mass = 2; \n", " elbowLeft.attachTo(shoulder, headLength*3/2, 1, bodyHeight*2, true);\n", " elbowRight.attachTo(shoulder, headLength*3/2, 1, bodyHeight*2, true);\n", " \n", " handLeft = new PointMass(x + random(-5,5),y + random(-5,5));\n", " handRight = new PointMass(x + random(-5,5),y + random(-5,5));\n", " handLeft.mass = 2;\n", " handRight.mass = 2;\n", " handLeft.attachTo(elbowLeft, headLength*2, 1, bodyHeight*2, true);\n", " handRight.attachTo(elbowRight, headLength*2, 1, bodyHeight*2, true);\n", " \n", " pelvis = new PointMass(x + random(-5,5),y + random(-5,5));\n", " pelvis.mass = 15; // pelvis to lower torso\n", " pelvis.attachTo(shoulder,headLength*3.5,0.8,bodyHeight*2, true);\n", " // this restraint keeps the head from tilting in extremely uncomfortable positions\n", " pelvis.attachTo(head, headLength*4.75, 0.02, bodyHeight*2, false);\n", " \n", " kneeLeft = new PointMass(x + random(-5,5),y + random(-5,5));\n", " kneeRight = new PointMass(x + random(-5,5),y + random(-5,5));\n", " kneeLeft.mass = 10;\n", " kneeRight.mass = 10;\n", " kneeLeft.attachTo(pelvis, headLength*2, 1, bodyHeight*2, true);\n", " kneeRight.attachTo(pelvis, headLength*2, 1, bodyHeight*2, true);\n", " \n", " footLeft = new PointMass(x + random(-5,5),y + random(-5,5));\n", " footRight = new PointMass(x + random(-5,5),y + random(-5,5));\n", " footLeft.mass = 5; // calf + foot\n", " footRight.mass = 5;\n", " footLeft.attachTo(kneeLeft, headLength*2, 1, bodyHeight*2, true);\n", " footRight.attachTo(kneeRight, headLength*2, 1, bodyHeight*2, true);\n", " \n", " // these constraints resist flexing the legs too far up towards the body\n", " footLeft.attachTo(shoulder, headLength*7.5, 0.001, bodyHeight*2, false);\n", " footRight.attachTo(shoulder, headLength*7.5, 0.001, bodyHeight*2, false);\n", " \n", " headCircle = new Circle(headLength*0.75);\n", " headCircle.attachToPointMass(head);\n", " \n", " world.addCircle(headCircle);\n", " addPointMass(head);\n", " addPointMass(shoulder);\n", " addPointMass(pelvis);\n", " addPointMass(elbowLeft);\n", " addPointMass(elbowRight);\n", " addPointMass(handLeft);\n", " addPointMass(handRight);\n", " addPointMass(kneeLeft);\n", " addPointMass(kneeRight);\n", " addPointMass(footLeft);\n", " addPointMass(footRight);\n", " }\n", " \n", " void removeFromWorld () {\n", " world.removeCircle(headCircle);\n", " removePointMass(head);\n", " removePointMass(shoulder);\n", " removePointMass(pelvis);\n", " removePointMass(elbowLeft);\n", " removePointMass(elbowRight);\n", " removePointMass(handLeft);\n", " removePointMass(handRight);\n", " removePointMass(kneeLeft);\n", " removePointMass(kneeRight);\n", " removePointMass(footLeft);\n", " removePointMass(footRight);\n", " }\n", "}\n", "\n", "// Timesteps are managed here\n", "class World {\n", " // list of circle constraints\n", " ArrayList circles = new ArrayList();\n", " \n", " long previousTime;\n", " long currentTime;\n", " \n", " int fixedDeltaTime;\n", " float fixedDeltaTimeSeconds;\n", " \n", " int leftOverDeltaTime;\n", " \n", " int constraintAccuracy;\n", " \n", " World() {\n", " fixedDeltaTime = 16;\n", " fixedDeltaTimeSeconds = (float)fixedDeltaTime / 1000.0;\n", " leftOverDeltaTime = 0;\n", " constraintAccuracy = 3;\n", " }\n", " \n", " // Update physics\n", " void update() {\n", " // calculate elapsed time\n", " currentTime = millis();\n", " long deltaTimeMS = currentTime - previousTime;\n", " \n", " previousTime = currentTime; // reset previous time\n", " \n", " // break up the elapsed time into manageable chunks\n", " int timeStepAmt = (int)((float)(deltaTimeMS + leftOverDeltaTime) / (float)fixedDeltaTime);\n", " \n", " // limit the timeStepAmt to prevent potential freezing\n", " timeStepAmt = min(timeStepAmt, 5);\n", " \n", " // store however much time is leftover for the next frame\n", " leftOverDeltaTime = (int)deltaTimeMS - (timeStepAmt * fixedDeltaTime);\n", " \n", " // How much to push PointMasses when the user is interacting\n", " mouseInfluenceScalar = 1.0 / timeStepAmt;\n", " \n", " // update physics\n", " for (int iteration = 1; iteration <= timeStepAmt; iteration++) {\n", " // solve the constraints multiple times\n", " // the more it's solved, the more accurate.\n", " for (int x = 0; x < constraintAccuracy; x++) {\n", " for (int i = 0; i < pointmasses.size(); i++) {\n", " PointMass pointmass = (PointMass) pointmasses.get(i);\n", " pointmass.solveConstraints();\n", " }\n", " for (int i = 0; i < circles.size(); i++) {\n", " Circle c = (Circle) circles.get(i);\n", " c.solveConstraints(); \n", " }\n", " }\n", " \n", " // update each PointMass's position\n", " for (int i = 0; i < pointmasses.size(); i++) {\n", " PointMass pointmass = (PointMass) pointmasses.get(i);\n", " pointmass.updateInteractions();\n", " pointmass.updatePhysics(fixedDeltaTimeSeconds);\n", " }\n", " }\n", " }\n", " \n", " void addCircle (Circle c) {\n", " circles.add(c); \n", " }\n", " void removeCircle (Circle c) {\n", " circles.remove(c); \n", " }\n", "}\n", "\n", "\n", "// Where we'll store all of the points\n", "ArrayList pointmasses;\n", "\n", "// every PointMass within this many pixels will be influenced by the cursor\n", "float mouseInfluenceSize = 20; \n", "// minimum distance for tearing when user is right clicking\n", "float mouseTearSize = 8;\n", "float mouseInfluenceScalar = 5;\n", "\n", "// amount to accelerate everything downward\n", "float gravity = 980; \n", "\n", "// Dimensions for our curtain. These are number of PointMasss for each direction, not actual widths and heights\n", "// the true width and height can be calculated by multiplying restingDistances by the curtain dimensions\n", "final int curtainHeight = 40;\n", "final int curtainWidth = 60;\n", "final int yStart = 25; // where will the curtain start on the y axis?\n", "final float restingDistances = 6;\n", "final float stiffnesses = 1;\n", "final float curtainTearSensitivity = 50; // distance the PointMasss have to go before ripping\n", "\n", "World world;\n", "\n", "void setup() {\n", " size(640,480);\n", " world = new World();\n", " mouseInfluenceSize = 20; \n", " // minimum distance for tearing when user is right clicking\n", " mouseTearSize = 8;\n", " mouseInfluenceScalar = 5;\n", " // we square the mouseInfluenceSize and mouseTearSize so we don't have to use squareRoot when comparing distances with this.\n", " mouseInfluenceSize *= mouseInfluenceSize; \n", " mouseTearSize *= mouseTearSize;\n", " reset();\n", "}\n", "\n", "void draw() {\n", " background(255); \n", " world.update();\n", " updateGraphics();\n", "}\n", "\n", "void updateGraphics() {\n", " for (PointMass p : pointmasses) {\n", " p.draw();\n", " }\n", " for (Circle c : world.circles) {\n", " c.draw(); \n", " }\n", "}\n", "\n", "void addPointMass(PointMass p) {\n", " pointmasses.add(p); \n", "}\n", "\n", "void removePointMass(PointMass p) {\n", " pointmasses.remove(p); \n", "}\n", "\n", "void createCurtain() {\n", " // midWidth: amount to translate the curtain along x-axis for it to be centered\n", " // (curtainWidth * restingDistances) = curtain's pixel width\n", " int midWidth = (int) (width/2 - (curtainWidth * restingDistances)/2);\n", " // Since this our fabric is basically a grid of points, we have two loops\n", " for (int y = 0; y <= curtainHeight; y++) { // due to the way PointMasss are attached, we need the y loop on the outside\n", " for (int x = 0; x <= curtainWidth; x++) { \n", " PointMass pointmass = new PointMass(midWidth + x * restingDistances, y * restingDistances + yStart);\n", " \n", " // attach to \n", " // x - 1 and\n", " // y - 1 \n", " // *<---*<---*<-..\n", " // ^ ^ ^\n", " // | | |\n", " // *<---*<---*<-..\n", " //\n", " // PointMass attachTo parameters: PointMass PointMass, float restingDistance, float stiffness\n", " // try disabling the next 2 lines (the if statement and attachTo part) to create a hairy effect\n", " if (x != 0) \n", " pointmass.attachTo((PointMass)(pointmasses.get(pointmasses.size()-1)), restingDistances, stiffnesses);\n", " // the index for the PointMasss are one dimensions, \n", " // so we convert x,y coordinates to 1 dimension using the formula y*width+x \n", " if (y != 0)\n", " pointmass.attachTo((PointMass)(pointmasses.get((y - 1) * (curtainWidth+1) + x)), restingDistances, stiffnesses);\n", " // we pin the very top PointMasss to where they are\n", " if (y == 0)\n", " pointmass.pinTo(pointmass.x, pointmass.y);\n", " // add to PointMass array \n", " pointmasses.add(pointmass);\n", " }\n", " }\n", "}\n", "\n", "void createBodies(int count) {\n", " for (int i = 0; i < count; i++) {\n", " new Body(random(width), random(height), 80);\n", " } \n", "}\n", "\n", "// Controls. The r key resets the curtain, g toggles gravity\n", "void keyPressed() {\n", " if ((key == 'r') || (key == 'R')) {\n", " reset();\n", " } \n", " if ((key == 'g') || (key == 'G'))\n", " toggleGravity();\n", "}\n", "\n", "void toggleGravity() {\n", " if (gravity != 0)\n", " gravity = 0;\n", " else\n", " gravity = 980;\n", "}\n", "\n", "// Using http://www.codeguru.com/forum/showpost.php?p=1913101&postcount=16\n", "// We use this to have consistent interaction\n", "// so if the cursor is moving fast, it won't interact only in spots where the applet registers it at\n", "float distPointToSegmentSquared(float lineX1, float lineY1, float lineX2, float lineY2, float pointX, float pointY) {\n", " float vx = lineX1 - pointX;\n", " float vy = lineY1 - pointY;\n", " float ux = lineX2 - lineX1;\n", " float uy = lineY2 - lineY1;\n", " \n", " float len = ux*ux + uy*uy;\n", " float det = (-vx * ux) + (-vy * uy);\n", " if ((det < 0) || (det > len)) {\n", " ux = lineX2 - pointX;\n", " uy = lineY2 - pointY;\n", " return min(vx*vx+vy*vy, ux*ux+uy*uy);\n", " }\n", " \n", " det = ux*vy - uy*vx;\n", " return (det*det) / len;\n", "}\n", "\n", "void reset() {\n", " pointmasses = new ArrayList();\n", " world.circles = new ArrayList();\n", " createCurtain();\n", " //createBodies(20);\n", "}" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Calysto Processing", "language": "processing", "name": "calysto_processing" }, "language_info": { "codemirror_mode": { "name": "text/x-java", "version": 2 }, "file_extension": ".java", "mimetype": "text/x-java", "name": "java" } }, "nbformat": 4, "nbformat_minor": 0 }